Skip to content

Reduce dashboard CPU under sustained metric ingestion - #20736

Merged
James Newton-King (JamesNK) merged 1 commit into
mainfrom
jamesnk/dashboard-metric-retention-cpu
Oct 5, 2026
Merged

James Newton-King (JamesNK) merged 1 commit into
mainfrom
jamesnk/dashboard-metric-retention-cpu

Conversation

@JamesNK

Copy link
Copy Markdown
Member

Description

Long-running AppHosts could consume steadily increasing dashboard CPU while ingesting a fixed metrics workload. Metric retention ranked every retained point in each changed dimension on every insertion, including when the dimension was still below its retention limit. This reproduces independently of exemplars.

Cache each dimension's retained point count and load it from SQLite when an existing dimension is first used. Skip retention cleanup below capacity; when capacity is exceeded, use the existing dimension-order index to delete only the oldest surplus points. Existing cascading foreign keys continue to delete the removed points' exemplars and filtered attributes in the same transaction.

Also:

  • Add behavioral coverage for dimension-specific retention, repeated values, database reopening, and recovery after a failed retention transaction.
  • Remove SQL-activity assertions and the SQL-only lookup test, retaining assertions on the returned telemetry.
  • Add AddHistogramMetricsAtCapacity, preloading 50,000 points per dimension and replaying four exemplars per point. History population and payload construction are outside the measured operation.
  • Update the metrics benchmark's schema initialization, async query API calls, and in-process toolchain so it can execute on the current .NET 11 SDK.
  • Document the ingestion benchmark.

User-facing usage

Continue starting the AppHost normally, for example with aspire start. No configuration changes are required. The metric retention limit, one-second development export interval, and exemplar behavior are unchanged; steady-state ingestion no longer scans the retained history on every insertion.

Benchmark comparison

Both variants used BenchmarkDotNet 0.15.8, .NET 11 Release builds, Windows x64, the same populated-history workload, three warmup iterations, and ten measured iterations. The baseline used the original retention source from commit 0a42e2c6ae; the fixed variant used this branch's implementation.

Dimensions Before mean/export After mean/export Approximate speedup Managed allocation before/after
1 20.77 ms 0.464 ms 45x 38.90 / 36.78 KB
5 133.09 ms 2.905 ms 46x 136.41 / 139.08 KB

The baseline used 20 operations per iteration. The fixed path used 512 operations per iteration to avoid short samples. Results are normalized per export, which adds one point per dimension. The fixed five-dimension result had a standard deviation of 1.073 ms, so the speedup figures should be treated as approximate.

Validation

  • Passed: 59 targeted tests, excluding quarantined and outerloop tests:
dotnet test --project tests\Aspire.Dashboard.Tests\Aspire.Dashboard.Tests.csproj --no-launch-profile /p:SkipNativeBuild=true /p:InstallBrowsersForPlaywright=false -- --filter-class "*.SqliteMetricsTests" --filter-class "*.SqliteTelemetryPersistenceTests" --filter-not-trait "quarantined=true" --filter-not-trait "outerloop=true"
  • Passed: focused baseline and fixed Release builds. The full benchmark project has pre-existing compile errors in unrelated span/trace benchmark classes that use removed APIs. A temporary MSBuild override excluded those classes for both builds; for the baseline only, it substituted the original metrics-write source without reverting the worktree fix. <scratch> below replaces the local temporary directory.
dotnet build benchmarks\Aspire.Dashboard.Benchmarks\Aspire.Dashboard.Benchmarks.csproj --configuration Release --no-restore /p:SkipNativeBuild=true /p:DirectoryBuildTargetsPath="<scratch>\issue20725-baseline.targets" /p:UseOriginalMetricRetention=true
dotnet build benchmarks\Aspire.Dashboard.Benchmarks\Aspire.Dashboard.Benchmarks.csproj --configuration Release --no-restore --no-incremental /p:SkipNativeBuild=true /p:DirectoryBuildTargetsPath="<scratch>\issue20725-baseline.targets" /p:UseOriginalMetricRetention=false
  • Passed: both ingestion benchmark cases with the original and fixed implementations:
dotnet exec artifacts\bin\Aspire.Dashboard.Benchmarks\Release\net11.0\Aspire.Dashboard.Benchmarks.dll --filter '*AddHistogramMetricsAtCapacity*' --strategy Throughput --launchCount 1 --warmupCount 3 --iterationCount 10 --invocationCount 20 --unrollFactor 1 --artifacts '<scratch>\issue20725-benchmark-before' --exporters json
dotnet exec artifacts\bin\Aspire.Dashboard.Benchmarks\Release\net11.0\Aspire.Dashboard.Benchmarks.dll --filter '*AddHistogramMetricsAtCapacity*' --strategy Throughput --launchCount 1 --warmupCount 3 --iterationCount 10 --invocationCount 512 --unrollFactor 1 --artifacts '<scratch>\issue20725-benchmark-after-precise' --exporters json
  • Passed: all 12 existing metrics-query benchmark cases as a smoke test:
dotnet exec artifacts\bin\Aspire.Dashboard.Benchmarks\Release\net11.0\Aspire.Dashboard.Benchmarks.dll --filter '*Get*Metrics*' --artifacts '<scratch>\issue20725-benchmark-query-smoke'
  • Passed: git diff --check.
  • The exact 15-service macOS workload and native AOT publish were not validated.
  • No new dependencies or database schema changes.

Fixes #20725

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
      • If yes, did you have an API Review for it?
        • Yes
        • No
      • Did you add <remarks /> and <code /> elements on your triple slash comments?
        • Yes
        • No
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
      • If yes, have you done a threat model and had a security review?
        • Yes
        • No
    • No

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 20736

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 20736"

@aspire-repo-bot
aspire-repo-bot Bot requested a balanced review from Copilot October 5, 2026 11:37
@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Tests selector

Selects the full PR test matrix + all PR-gated jobs (ALL) — run-all fallback: 'benchmarks/Aspire.Dashboard.Benchmarks/TelemetryRepositoryMetricsBenchmarks.cs' is neither Layer-1-owned nor matched by a Layer 2 rule


Selection computed for commit 96fcd45.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The optimized retention logic preserves existing semantics and has focused coverage for capacity, persistence, repeated values, and transaction failure recovery.

Review effort: Balanced
Findings: None

What changed in this PR

Optimizes SQLite metric retention to prevent rising dashboard CPU during sustained ingestion.

Changes:

  • Caches per-dimension point counts and performs indexed surplus deletion.
  • Adds retention, restart, rollback, and multi-dimension tests.
  • Adds and documents a capacity-ingestion benchmark.
File Description
src/​Aspire.Dashboard/​Otlp/​Storage/​SqliteTelemetryRepository.Metrics.Writes.cs Optimizes metric retention.
tests/​Aspire.Dashboard.Tests/​TelemetryRepositoryTests/​MetricsTests.cs Covers dimension-specific retention.
tests/​Aspire.Dashboard.Tests/​TelemetryRepositoryTests/​SqliteTelemetryPersistenceTests.cs Covers reopening and rollback recovery.
benchmarks/​Aspire.Dashboard.Benchmarks/​TelemetryRepositoryMetricsBenchmarks.cs Adds capacity-ingestion benchmarking.
docs/​specs/​dashboard-persistence.md Documents the benchmark.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Retrying the failed CI jobs for this pull request from the CI run attempt. The rerun is being tracked in the rerun attempt.

@JamesNK
James Newton-King (JamesNK) merged commit 833be62 into main Oct 5, 2026
1248 of 1257 checks passed
@JamesNK

Copy link
Copy Markdown
Member Author

/backport to release/13.6

@github-actions

github-actions Bot commented Oct 5, 2026

Copy link
Copy Markdown
Contributor

Started backporting to release/13.6 (link to workflow run)

@aspire-repo-bot

Copy link
Copy Markdown
Contributor

✅ No documentation update needed.

Step 5 branch: docs_required → proven false positive (no actual user-facing change).

Triggered signals (2): pr_body_has_cli_flag_mention, pr_body_has_user_facing_section.

  • pr_body_has_cli_flag_mention — evidence hint is dotnet test --project tests\Aspire.Dashboard.Tests\Aspire.Dashboard.Tests.csproj ... from the PR's Validation section. This is a test-invocation command the author ran to prove the fix, not a CLI flag exposed to end users. No -- option was added to any Aspire CLI command.
  • pr_body_has_user_facing_section — the PR's own ### User-facing usage heading states verbatim: "Continue starting the AppHost normally, for example with aspire start. No configuration changes are required. The metric retention limit, one-second development export interval, and exemplar behavior are unchanged; steady-state ingestion no longer scans the retained history on every insertion." The author explicitly confirms there is no behavior change visible to users — only an internal CPU/allocation optimization.

Diff confirms no public surface change: the only src/ file touched is src/Aspire.Dashboard/Otlp/Storage/SqliteTelemetryRepository.Metrics.Writes.cs, which caches a per-dimension point count and changes the SQL used to trim excess metric points (replacing a window-function rank scan with an indexed DELETE ... ORDER BY point_id LIMIT). No new/changed public types, options, config keys, or CLI surface. The remaining changed files are a benchmark (benchmarks/Aspire.Dashboard.Benchmarks/TelemetryRepositoryMetricsBenchmarks.cs), two test files, and an internal engineering spec (docs/specs/dashboard-persistence.md, in microsoft/aspire, not published on microsoft/aspire.dev) describing the new benchmark's methodology.

Retention limits, defaults, and dashboard behavior are already correctly documented on microsoft/aspire.dev; this PR does not change any of that documented behavior, so no docs update is needed there.

Jose Perez Rodriguez (joperezr) pushed a commit that referenced this pull request Oct 6, 2026
…20747)

Backport of #20736 to release/13.6

/cc @JamesNK

## Customer Impact

In Aspire 13.6, long-running AppHosts with sustained metric ingestion
can consume steadily increasing dashboard CPU even under a fixed or idle
workload. A reported 15-project AppHost reached approximately 70% of one
core after 6.8 hours, with significant SQLite churn and database growth.

## Testing

Passed 59 targeted SQLite metrics and persistence tests, excluding
quarantined and outerloop tests. The source PR also validated the
before/after ingestion benchmarks and smoke-tested all 12 existing
metrics-query benchmark cases. Backport PR CI is currently in progress.

## Risk

Low. The change is localized to SQLite metric-retention cleanup,
preserves the existing retention limit and cascading deletes, and adds
focused coverage for retention, database reopening, and
transaction-failure recovery. It makes no public API or database schema
changes.

## Regression?

Yes — introduced in 13.6 with SQLite-backed telemetry in #18924.

Co-authored-by: James Newton-King <james@newtonking.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This was referenced Oct 11, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Dashboard CPU grows steadily under idle metrics ingestion in 13.6 (SQLite exemplar churn)

3 participants